#! /bin/bash
#
# podman-registry - start/stop/monitor a local instance of registry:2
#
ME=$(basename $0)

###############################################################################
# BEGIN defaults

PODMAN_REGISTRY_IMAGE=quay.io/libpod/registry:2.8

PODMAN_REGISTRY_USER=
PODMAN_REGISTRY_PASS=
PODMAN_REGISTRY_PORT=

# Podman binary to run
PODMAN=${PODMAN:-$(dirname $0)/../bin/podman}

# END   defaults
###############################################################################
# BEGIN help messages

missing=" argument is missing; see $ME -h for details"
usage="Usage: $ME [options] [start|stop|ps|logs]

$ME manages a local instance of a container registry.

When called to start a registry, $ME will pull an image
into a local temporary directory, create an htpasswd, start the
registry, and dump a series of environment variables to stdout:

    \$ $ME start
    PODMAN_REGISTRY_IMAGE=\"docker.io/library/registry:2.8\"
    PODMAN_REGISTRY_PORT=\"5050\"
    PODMAN_REGISTRY_USER=\"userZ3RZ\"
    PODMAN_REGISTRY_PASS=\"T8JVJzKrcl4p6uT\"

Expected usage, therefore, is something like this in a script

    eval \$($ME start)

To stop the registry, you will need to know the port number:

    $ME -P \$PODMAN_REGISTRY_PORT stop

Override the default image, port, user, password with:

  -i IMAGE      registry image to pull (default: $PODMAN_REGISTRY_IMAGE)
  -u USER       registry user (default: random)
  -p PASS       password for registry user (default: random)
  -P PORT       port to bind to (on 127.0.0.1) (default: random, 5000-5999)

Other options:

  -h            display usage message
"

die () {
    echo "$ME: $*" >&2
    exit 1
}

# END   help messages
###############################################################################
# BEGIN option processing

while getopts "i:u:p:P:hv" opt; do
    case "$opt" in
        i)         PODMAN_REGISTRY_IMAGE=$OPTARG ;;
        u)         PODMAN_REGISTRY_USER=$OPTARG  ;;
        p)         PODMAN_REGISTRY_PASS=$OPTARG  ;;
        P)         PODMAN_REGISTRY_PORT=$OPTARG  ;;
        h)         echo "$usage"; exit 0;;
        v)         verbose=1 ;;
        \?)        echo "Run '$ME -h' for help" >&2; exit 1;;
    esac
done
shift $((OPTIND-1))

# END   option processing
###############################################################################
# BEGIN helper functions

function random_string() {
    local length=${1:-10}

    head /dev/urandom | tr -dc a-zA-Z0-9 | head -c$length
}

function podman() {
    if [ -z "${PODMAN_REGISTRY_PORT}" ]; then
        die "podman port undefined; please invoke me with -P PORT"
    fi

    if [ -z "${PODMAN_REGISTRY_WORKDIR}" ]; then
        PODMAN_REGISTRY_WORKDIR=${TMPDIR:-/tmp}/podman-registry-${PODMAN_REGISTRY_PORT}
        if [ ! -d ${PODMAN_REGISTRY_WORKDIR} ]; then
            die "$ME: directory does not exist: ${PODMAN_REGISTRY_WORKDIR}"
        fi
    fi

    # Reset $PODMAN, so ps/logs/stop use same args as the initial start
    PODMAN="$(<${PODMAN_REGISTRY_WORKDIR}/PODMAN)"

    ${PODMAN} --root    ${PODMAN_REGISTRY_WORKDIR}/root        \
              --runroot ${PODMAN_REGISTRY_WORKDIR}/runroot     \
              "$@"
}

###############
#  must_pass  #  Run a command quietly; abort with error on failure
###############
function must_pass() {
    local log=${PODMAN_REGISTRY_WORKDIR}/log

    "$@" &> $log
    if [ $? -ne 0 ]; then
        echo "$ME: Command failed: $*" >&2
        cat $log                       >&2

        # If we ever get here, it's a given that the registry is not running.
        # Clean up after ourselves.
        ${PODMAN} unshare rm -rf ${PODMAN_REGISTRY_WORKDIR}
        exit 1
    fi
}

###################
#  wait_for_port  #  Returns once port is available on localhost
###################
function wait_for_port() {
    local port=$1                      # Numeric port

    local host=127.0.0.1
    local _timeout=5

    # Wait
    while [ $_timeout -gt 0 ]; do
        { exec {unused_fd}<> /dev/tcp/$host/$port; } &>/dev/null && return
        sleep 1
        _timeout=$(( $_timeout - 1 ))
    done

    die "Timed out waiting for port $port"
}

# END   helper functions
###############################################################################
# BEGIN action processing

function do_start() {
    # If called without a port, assign a random one in the 5xxx range
    if [ -z "${PODMAN_REGISTRY_PORT}" ]; then
        for port in $(shuf -i 5000-5999);do
            if ! { exec {unused_fd}<> /dev/tcp/127.0.0.1/$port; } &>/dev/null; then
                PODMAN_REGISTRY_PORT=$port
                break
            fi
        done
    fi

    PODMAN_REGISTRY_WORKDIR=${TMPDIR:-/tmp}/podman-registry-${PODMAN_REGISTRY_PORT}
    if [ -d ${PODMAN_REGISTRY_WORKDIR} ]; then
        die "$ME: directory exists: ${PODMAN_REGISTRY_WORKDIR} (another registry might already be running on this port)"
    fi

    # Randomly-generated username and password, if none given on command line
    if [ -z "${PODMAN_REGISTRY_USER}" ]; then
        PODMAN_REGISTRY_USER="user$(random_string 4)"
    fi
    if [ -z "${PODMAN_REGISTRY_PASS}" ]; then
        PODMAN_REGISTRY_PASS=$(random_string 15)
    fi

    # For the next few commands, die on any error
    set -e

    mkdir -p ${PODMAN_REGISTRY_WORKDIR}

    # Preserve initial podman path & args, so all subsequent invocations
    # of this script are consistent with the first one.
    echo "$PODMAN" >${PODMAN_REGISTRY_WORKDIR}/PODMAN

    local AUTHDIR=${PODMAN_REGISTRY_WORKDIR}/auth
    mkdir -p $AUTHDIR

    # Pull registry image, but into a separate container storage
    mkdir -p ${PODMAN_REGISTRY_WORKDIR}/root
    mkdir -p ${PODMAN_REGISTRY_WORKDIR}/runroot

    set +e

    # Give it three tries, to compensate for flakes
    podman pull ${PODMAN_REGISTRY_IMAGE}      &>/dev/null ||
        podman pull ${PODMAN_REGISTRY_IMAGE}  &>/dev/null ||
        must_pass podman pull ${PODMAN_REGISTRY_IMAGE}

    # Registry image needs a cert. Self-signed is good enough.
    local CERT=$AUTHDIR/domain.crt
    must_pass openssl req -newkey rsa:4096 -nodes -sha256              \
              -keyout ${AUTHDIR}/domain.key -x509 -days 2              \
              -out ${AUTHDIR}/domain.crt                               \
              -subj "/C=US/ST=Foo/L=Bar/O=Red Hat, Inc./CN=localhost"

    # Store credentials where container will see them. We can't run
    # this one via must_pass because we need its stdout.
    htpasswd -Bbn ${PODMAN_REGISTRY_USER} ${PODMAN_REGISTRY_PASS}   \
           > $AUTHDIR/htpasswd
    if [ $? -ne 0 ]; then
        rm -rf ${PODMAN_REGISTRY_WORKDIR}
        die "Command failed: htpasswd"
    fi

    # In case someone needs to debug
    echo "${PODMAN_REGISTRY_USER}:${PODMAN_REGISTRY_PASS}" \
         > $AUTHDIR/htpasswd-plaintext

    # Run the registry container.
    must_pass podman run --quiet -d                                     \
              -p ${PODMAN_REGISTRY_PORT}:5000                           \
              --name registry                                           \
              -v $AUTHDIR:/auth:Z                                       \
              -e "REGISTRY_AUTH=htpasswd"                               \
              -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm"          \
              -e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd"           \
              -e "REGISTRY_HTTP_TLS_CERTIFICATE=/auth/domain.crt"       \
              -e "REGISTRY_HTTP_TLS_KEY=/auth/domain.key"               \
              ${PODMAN_REGISTRY_IMAGE}

    # Confirm that registry started and port is active
    wait_for_port $PODMAN_REGISTRY_PORT

    # Dump settings. Our caller will use these to access the registry.
    for v in IMAGE PORT USER PASS; do
        echo "PODMAN_REGISTRY_${v}=\"$(eval echo \$PODMAN_REGISTRY_${v})\""
    done
}


function do_stop() {
    podman stop registry
    podman rm -f registry

    # Use straight podman, not our alias function, to avoid 'overlay: EBUSY'
    cmd="rm -rf ${PODMAN_REGISTRY_WORKDIR}"
    if [[ $(id -u) -eq 0 ]]; then
        $cmd
    else
        ${PODMAN} unshare $cmd
    fi
}


function do_ps() {
    podman ps -a
}


function do_logs() {
    podman logs registry
}

# END   action processing
###############################################################################
# BEGIN command-line processing

# First command-line arg must be an action
action=${1?ACTION$missing}
shift

case "$action" in
    start)  do_start  ;;
    stop)   do_stop   ;;
    ps)     do_ps     ;;
    logs)   do_logs   ;;
    *)      die "Unknown action '$action'; must be start / stop / ps / logs" ;;
esac

# END   command-line processing
###############################################################################

exit 0
